<?php
namespace DRY\Authentication;

/**
 * Authentication Credentials storage using a database
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/authentication.html#database
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Authentication
 */
class Storage_Database implements Storage
{

	protected $CFG;

	/**
	 * Database module used (connection)
	 * @var \DRY\Module\Database
	 */
	private $db;
	/**
	 * QueryFactory module used
	 * @var \DRY\Module\Queryfactory
	 */
	private $qf;

	/**
	 * Credentials data
	 * @var array
	 */
	protected $cdata = array();

	/**
	 * Storage system constructor
	 * @param \SimpleXMLElement $CFG
	 * @param \DRY\Module\Authentication $auth_module
	 */
	public function __construct($CFG, \DRY\Module\Authentication &$auth_module)
	{
		$this-> CFG = $CFG;
		$this-> set_Password_Encryption($CFG-> encryption?:1); // set type of password encryption
		$this-> db = $auth_module[(string)$CFG-> connection];
		$this-> qf = $auth_module[(string)$CFG-> queryfactory];

		// when there's an active session, it means the user is already logged in
		if($auth_module-> start) {
			$this-> cdata['username'] = $auth_module-> account;
			$this-> cdata['id'] = $auth_module-> credentials_extra['id'];
			$this-> passed_validation = true;
		}
	}

	/**
	 * What function to use when transforming the password (prior to registering, checking or updating)
	 * @var {callback}
	 */
	private $password_transform_function;

	/**
	 * Returns the incoming password as-is
	 * @param string $password
	 */
	protected function password_No_Transform($password)
	{
		return $password;
	}

	/**
	 * Hashes the incoming password before sending it to the storage
	 * @param string $password
	 */
	protected function password_Hash($password, $account)
	{
		$algo = $this-> CFG-> encryption['algo']?:'sha256';
		if(!isset($this-> cdata['salt']))
			$this-> cdata['salt'] = pack('f', mt_rand() / mt_getrandmax());
		return hash($algo, base64_encode($this-> cdata['salt']). $account. $password, true); // merges a float number between 0 and 1 (packed, base64 encoded) with the account name and the password, and creates a hash out of it
	}

	/**
	 * @see DRY\Authentication\Storage::set_Password_Encryption()
	 */
	public function set_Password_Encryption($mixed=true)
	{
		if(is_callable($mixed))
			$this-> password_transform_function = $mixed;
		elseif((bool)$mixed) {
			$this-> password_transform_function = array($this, 'password_Hash');
		} else
			$this-> password_transform_function = array($this, 'password_No_Transform');
	}

	/**
	 * @see DRY\Authentication\Storage::create_Credentials()
	 * @return bool True = success, False = fail
	 */
	public function create_Credentials($account, $password, $status=0, $extra=array())
	{
		// create the credentials data that will be stored
		if(!is_array($extra)) $extra = array();

		$credentials = array(
			'username' => $account,
			'security' => call_user_func($this-> password_transform_function, $password, $account, $this-> CFG),
			'status' => $status
		);
		$this-> cdata = array_merge($this-> cdata, $extra, $credentials);

		// Check if $account already exists and return an error
		// TODO - Use a callback to check for other parameters as well ( insert into QF )
		$q = $this-> qf-> select()
			-> table($this-> CFG-> table)
			-> where('username = ?', $account)
			-> limit(1);
		$r = $this-> db-> query($q);
		if(count($r)) {
			// account already exists
			return false;
		}

		$q = $this-> qf-> insert()
			-> table($this-> CFG-> table)
			-> values($this-> cdata);
		$this-> db-> query($q);
		return true;
	}

	private $passed_validation = false;

	/**
	 * @see DRY\Authentication\Storage::check_Credentials()
	 */
	public function check_Credentials($account, $password)
	{
		$this-> passed_validation = false;
		$q = $this-> qf-> select()
			-> table($this-> CFG-> table)
			-> where('username = ?', $account)
			-> limit(1);
		$r = $this-> db-> query($q);
		if(count($r)) {
			$this-> cdata = $r-> current();
			if($this-> cdata['security'] == call_user_func($this-> password_transform_function, $password, $account, $this-> CFG)) {
				$this-> passed_validation = true;
				return $this-> cdata['status'];
			}
		}
		return false;
	}

	public function update_Credentials($account=null, $status=null, $password=null)
	{
	}

	/**
	 * @see DRY\Authentication\Storage::delete_Credentials()
	 */
	public function delete_Credentials($account=null)
	{
		if(is_null($account) && !$this-> passed_validation)
			return false;

		if(is_null($account)) {
			$account = $this-> cdata['username'];
			$this-> passed_validation = false;
			$this-> cdata = array();
		}
		$q = $this-> qf-> delete()
			-> table($this-> CFG-> table)
			-> where('username = ?', $account)
			-> limit(1);
		$this-> db-> query($q);
	}

	/**
	 * @see DRY\Authentication\Storage::read_Extra()
	 */
	public function read_Extra($account=null)
	{
		if(is_null($account) && !$this-> passed_validation)
			return null;

		if(!is_null($account)) {
			$q = $this-> qf-> select()
				-> table($this-> CFG-> table)
				-> where('username = ?', $account)
				-> limit(1);
			$data = $this-> db-> query($q)-> current();
			if(empty($data))
				return null;
		} else
			$data = $this-> cdata;
		unset($data['username'], $data['security'], $data['salt']);
		return $data;
	}

	/**
	 * @see DRY\Authentication\Storage::update_Extra()
	 */
	public function update_Extra($extra=null, $account=null)
	{
		if(is_null($account) && !$this-> passed_validation)
			return false;

		unset($extra['username'], $extra['security'], $extra['salt'], $extra['status']); // disable changing credentials with this method

		if(is_null($account))
			$account = $this-> cdata['username'];

		// update the database
		$q = $this-> qf-> update()
			-> table($this-> CFG-> table)
			-> where('username = ?', $account)
			-> values($extra)
			-> limit(1);
		$this-> db-> query($q);

		return true;
	}
}

/* Example config:
	<storage type='database'>
		<encryption algo='sha256'>1</encryption>
		<connection>database</connection>
		<queryfactory>qf</queryfactory>
		<table>accounts</table>
	</storage>
 */